/*
 * Copyright 2011 The Rusted Roof, LLC.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *        http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */

package org.proteusframework.runtime;

import org.proteusframework.extension.IAdapterFactory;
import org.proteusframework.extension.IAdapterManager;
import org.proteusframework.extension.IAdapterManagerProvider;
import org.proteusframework.util.Assert;

import java.util.*;

/**
 * Provides an Eclipse-like Adapter Manager that supports a wide array of
 * {@link org.proteusframework.extension.IAdaptable} implementations.
 *
 * @author Tacoma Four
 */
public final class AdapterManager implements IAdapterManager
{
    /**
     * Cache of class lookup order (Class -> Class[]). This avoids having to compute often, and
     * provides clients with quick lookup for instanceOf checks based on type name.
     * Thread safety note: The map is synchronized using a synchronized
     * map wrapper class.  The arrays within the map are immutable.
     */
    private Map classSearchOrderLookup;

    /**
     * Map of registrar, keyed by <code>String</code>, fully qualified class name of
     * the adaptable class that the factory provides adapters for. Value is a <code>List</code>
     * of <code>IAdapterFactory</code>.
     */
    private final HashMap factories;

    /**
     * Cache of adapters for a given adaptable class. Maps String  -> Map
     * (adaptable class name -> (adapter class name -> factory instance))
     * Thread safety note: The outer map is synchronized using a synchronized
     * map wrapper class.  The inner map is not synchronized, but it is immutable
     * so synchronization is not necessary.
     */
    private Map adapterLookup;

    private final ArrayList lazyFactoryProviders;

    private static final AdapterManager singleton = new AdapterManager();

    /**
     * Private constructor to block instance creation.
     */
    private AdapterManager()
    {
        factories = new HashMap(5);
        lazyFactoryProviders = new ArrayList(1);
    }

    static AdapterManager getInstance()
    {
        return singleton;
    }

    /**
     * Computes the adapters that the provided class can adapt to, along
     * with the factory object that can perform that transformation. Returns
     * a table of adapter class name to factory object.
     *
     * @param adaptable adaptable class
     * @return Map of registrar associated with the adaptable class
     */
    @SuppressWarnings("unchecked")
    private Map getFactories(Class adaptable)
    {
        //cache reference to lookup to protect against concurrent flush
        Map lookup = adapterLookup;

        if (lookup == null)
            adapterLookup = lookup = Collections.synchronizedMap(new HashMap(30));

        Map table = (Map) lookup.get(adaptable.getName());
        if (table == null)
        {
            // calculate adapters for the class
            table = new HashMap(4);

            Class[] classes = computeClassOrder(adaptable);

            for (Class aClass : classes)
                addFactoriesFor(aClass.getName(), table);

            // cache the table
            lookup.put(adaptable.getName(), table);
        }
        return table;
    }


    /**
     * Given a type name, add all of the registrar that respond to those types into
     * the given table. Each entry will be keyed by the adapter class name (supplied in
     * IAdapterFactory.getAdapterList).
     *
     * @param typeName type
     * @param table    table
     */
    @SuppressWarnings("unchecked")
    private void addFactoriesFor(String typeName, Map table)
    {
        List factoryList = (List) getFactories().get(typeName);
        if (factoryList == null)
            return;
        for (int i = 0, imax = factoryList.size(); i < imax; i++)
        {
            IAdapterFactory factory = (IAdapterFactory) factoryList.get(i);
            Class[] adapters = factory.getAdapterList();
            for (Class adapter : adapters)
            {
                String adapterName = adapter.getName();
                if (table.get(adapterName) == null)
                    table.put(adapterName, factory);
            }
        }
    }

    public HashMap getFactories()
    {
        synchronized (lazyFactoryProviders)
        {
            while (lazyFactoryProviders.size() > 0)
            {
                IAdapterManagerProvider provider = (IAdapterManagerProvider) lazyFactoryProviders.remove(0);
                if (provider.addFactories(this))
                    flushLookup();
            }
        }
        return factories;
    }


    /**
     * Returns the super-type search order starting with <code>adaptable</code>.
     * The search order is defined in this class' comment.
     *
     * @param adaptable adaptable class
     * @return Array of classes in use of order
     */
    @SuppressWarnings("unchecked")
    public Class[] computeClassOrder(Class adaptable)
    {
        Class[] classes = null;
        //cache reference to lookup to protect against concurrent flush
        Map lookup = classSearchOrderLookup;

        if (lookup == null)
            classSearchOrderLookup = lookup = Collections.synchronizedMap(new HashMap());
        else
            classes = (Class[]) lookup.get(adaptable);

        // compute class order only if it hasn't been cached before
        if (classes == null)
        {
            classes = doComputeClassOrder(adaptable);
            lookup.put(adaptable, classes);
        }
        return classes;
    }

    /**
     * Computes the super-type search order starting with <code>adaptable</code>.
     * The search order is defined in this class' comment.
     *
     * @param adaptable Adapatable class
     * @return Computed class order
     */
    @SuppressWarnings("unchecked")
    private Class[] doComputeClassOrder(Class adaptable)
    {
        List classes = new ArrayList();
        Class clazz = adaptable;
        Set seen = new HashSet(4);
        //first traverse class hierarchy
        while (clazz != null)
        {
            classes.add(clazz);
            clazz = clazz.getSuperclass();
        }
        //now traverse interface hierarchy for each class
        Class[] classHierarchy = (Class[]) classes.toArray(new Class[classes.size()]);

        for (Class aClassHierarchy : classHierarchy)
            computeInterfaceOrder(aClassHierarchy.getInterfaces(), classes, seen);

        return (Class[]) classes.toArray(new Class[classes.size()]);
    }


    @SuppressWarnings("unchecked")
    private void computeInterfaceOrder(Class[] interfaces, Collection classes, Set seen)
    {
        List newInterfaces = new ArrayList(interfaces.length);
        for (Class interfac : interfaces)
        {
            if (seen.add(interfac))
            {
                //note we cannot recurse here without changing the resulting interface order
                classes.add(interfac);
                newInterfaces.add(interfac);
            }
        }
        for (Object newInterface : newInterfaces)
            computeInterfaceOrder(((Class) newInterface).getInterfaces(), classes, seen);
    }


    @Override
    public Object getAdapter(Object adaptable, Class adapterType)
    {
        Assert.parameterNotNull(adaptable, "Parameter 'adaptable' must not be null");
        Assert.parameterNotNull(adapterType, "Parameter 'adatperType' must not be null");

        IAdapterFactory factory = (IAdapterFactory) getFactories(adaptable.getClass()).get(adapterType.getName());

        Object result = null;

        if (factory != null)
            result = factory.getAdapter(adaptable, adapterType);

        if (result == null && adapterType.isInstance(adaptable))
            return adaptable;

        return result;
    }

    /**
     * Flushes the cache of adapter search paths. This is generally required whenever an
     * adapter is added or removed.
     * <p>
     * It is likely easier to just toss the whole cache rather than trying to be smart
     * and remove only those entries affected.
     * </p>
     */
    public synchronized void flushLookup()
    {
        adapterLookup = null;
        classSearchOrderLookup = null;
    }

    /*
     * @see IAdapterManager#registerAdapters
     */

    @Override
    public synchronized void registerAdapter(IAdapterFactory factory, Class adaptable)
    {
        registerFactory(factory, adaptable.getName());
        flushLookup();
    }

    /*
     * @see IAdapterManager#registerAdapters
     */

    @SuppressWarnings("unchecked")
    public void registerFactory(IAdapterFactory factory, String adaptableType)
    {
        List list = (List) factories.get(adaptableType);
        if (list == null)
        {
            list = new ArrayList(5);
            factories.put(adaptableType, list);
        }
        list.add(factory);
    }


    /*
     * @see IAdapterManager#unregisterAdapters
     */

    public synchronized void unregisterAdapter(IAdapterFactory factory, Class adaptable)
    {
        List factoryList = (List) factories.get(adaptable.getName());
        if (factoryList == null)
            return;
        factoryList.remove(factory);
        flushLookup();
    }

    /*
     * @see IAdapterManager#unregisterAdapters
     */

    public synchronized void unregisterAdapter(IAdapterFactory factory)
    {
        for (Object o : factories.values()) ((List) o).remove(factory);
        flushLookup();
    }


}
